package com.googlecode.wmlynar.springconfgraph;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedHashSet;

import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.JavaDocBuilder.ErrorHandler;
import com.thoughtworks.qdox.model.Annotation;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.parser.ParseException;

public class GraphStringBuilder {

	public static boolean includeUtility = true;

	public static void main(String[] args) throws IOException {
		ArrayList<String> folders = new ArrayList<String>();
		folders.add("src/main/java");

		String output = buildArchitectureString(folders);

		PrintWriter out = new PrintWriter("target/confluence_architecture.txt");

		try {
			out.println(output);
		} finally {
			out.close();
		}
	}

	/**
	 * Builds string with architecture build out of all configuration classes
	 */
	static String buildArchitectureString(ArrayList<String> folders) {
		JavaDocBuilder builder = instantiateJavadocBuilder(folders);

		ArrayList<JavaClass> configurationClasses = findConfigurationClasses(builder);

		String output = "";

		output += buildLegendString();
		output += buildAllConfigObjectsString(builder, configurationClasses);

		return output;
	}

	/**
	 * Creates builder and scans all folders given as parameters.
	 */
	private static JavaDocBuilder instantiateJavadocBuilder(
			ArrayList<String> folders) {
		JavaDocBuilder builder = new JavaDocBuilder();
		builder.setErrorHandler(new ErrorHandler() {
			@Override
			public void handle(ParseException arg0) {
				System.err.println(arg0.getMessage());
			}
		});
		for (String folder : folders) {
			builder.addSourceTree(new File(folder));
		}
		return builder;
	}

	/**
	 * Returns string that is a legend of colors used for objects.
	 */
	private static String buildLegendString() {
		String legend = "h3. Legend\n\n"
				+ "{graph-from-table:direction=TB|node=fontsize=10|edge=fontsize=8|}\n"
				+ "| bean |  |  | fillcolor=\"lightyellow\" |\n"
				+ "| controller |  |  | fillcolor=\"green\" |\n"
				+ "| service |  |  | fillcolor=\"red\" |\n"
				+ "| external |  |  | fillcolor=\"yellow\" |\n"
				+ "| utility |  |  | fillcolor=\"cyan\" |\n"
				+ "{graph-from-table}\n\n";

		if (!includeUtility) {
			legend += "Utility classes excluded for simplicity\n";
		}

		return legend;
	}

	/**
	 * Builds string with architecture for all configuration objects.
	 */
	private static String buildAllConfigObjectsString(JavaDocBuilder builder,
			ArrayList<JavaClass> configurationClasses) {
		String output = "";

		for (JavaClass c : configurationClasses) {
			output += "h3. " + c.getName() + "\n\n";
			output += buikdConfigObjectString(builder,
					c.getFullyQualifiedName());
			output += "\n";
		}

		return output;
	}

	/**
	 * externally referenced object should be colored in yellow because they
	 * will not have their own table row they will only be referenced by other
	 * objects (that will not change their colour)
	 * 
	 * then attributes should be colored cyan because they will be listed next
	 * (referencing object can not change their color)
	 * 
	 * then all other objects should be colored depending on their naming
	 * 
	 * the name of the bean should be the same as variable of the bean or
	 * attribute it their name is the same as the type. if their name is
	 * different than type the type should be written below
	 */
	private static String buikdConfigObjectString(JavaDocBuilder builder,
			String configClassName) {
		String output = "{graph-from-table:direction=LR|node=fontsize=10,fillcolor=yellow|edge=fontsize=8|}\n";

		JavaClass configClass = builder.getClassByName(configClassName);

		ArrayList<JavaField> beanFields = getConfigAutowiredFields(configClass);
		ArrayList<JavaMethod> beanMethods = getConfigBeanMethods(configClass);

		output += buildFieldsString(beanFields);

		output += buildMethodObjectsString(beanMethods, beanFields);

		output += "{graph-from-table}\n";

		return output;
	}

	/**
	 * Build architecture string for all configuration methods annotated as
	 * bean.
	 */
	private static String buildMethodObjectsString(
			ArrayList<JavaMethod> configurationMethods,
			ArrayList<JavaField> configurationFields) {
		String output = "";
		for (JavaMethod method : configurationMethods) {
			String printedMethodName = getFullName(method);

			LinkedHashSet<String> connections = getObjectConnections(method,
					configurationMethods, configurationFields);

			// remove itself
			connections.remove(printedMethodName);

			String color = getColorForObjectName(method.getName());
			String colorParameter = "fillcolor=\"" + color + "\"";

			output += buildMethodConnectionsString(printedMethodName,
					connections, colorParameter);

		}
		return output;
	}

	/**
	 * if method does not have any parameters we need to add explicit object for
	 * it otherwise add connection to all other objects
	 */
	private static String buildMethodConnectionsString(
			String printedMethodName, LinkedHashSet<String> connections,
			String colorParameter) {
		String output = "";

		if (connections.isEmpty()) {
			output += "| " + printedMethodName + " |  |  | " + colorParameter
					+ " |\n";
		} else {
			for (String connection : connections) {
				output += "| " + printedMethodName + " | " + connection
						+ " | | " + colorParameter + " |\n";
			}
		}
		return output;
	}

	/**
	 * Returns all connections of particular object. Parameters, fields and
	 * other methods referenced in method body (if just by string comparison).
	 * Self can be part of the connection list as well;
	 */
	private static LinkedHashSet<String> getObjectConnections(
			JavaMethod method, ArrayList<JavaMethod> beanMethods,
			ArrayList<JavaField> beanFields) {
		LinkedHashSet<String> connections = new LinkedHashSet<String>();

		// add method parameters to connections
		for (JavaParameter parameter : method.getParameters()) {
			// if annotated @Value add value
			String value = getAnnotationValue(parameter);
			if (value != null) {
				connections.add(parameter.getName() + "="
						+ escapeValueNameForConfluence(value));
			} else {
				connections.add(getFullName(parameter));
			}
		}
		// add local variables that exist in code block
		String codeBlock = method.getCodeBlock();
		for (JavaField field : beanFields) {
			if (codeBlock.contains(field.getName())) {
				connections.add(getFullName(field));
			}
		}
		// add other beans that exist in code block
		for (JavaMethod bean : beanMethods) {
			if (codeBlock.contains(bean.getName())) {
				connections.add(getFullName(bean));
			}
		}
		return connections;
	}

	/**
	 * Returns list of class fields that are annotated as autowired. The
	 * assumption is that those fields are beans.
	 */
	private static ArrayList<JavaField> getConfigAutowiredFields(
			JavaClass configClass) {
		ArrayList<JavaField> beanFields = new ArrayList<JavaField>();

		// can switch off including utility classes for clarity
		if (includeUtility) {
			for (JavaField attribute : configClass.getFields()) {
				if (isAutowired(attribute)) {
					beanFields.add(attribute);
				}
			}
		}
		return beanFields;
	}

	/**
	 * Build architecture string for all fields. Only fields marked as autowired
	 * shoulf be passed here.
	 */
	private static String buildFieldsString(ArrayList<JavaField> beanFields) {
		String output = "";
		for (JavaField field : beanFields) {
			output += "| " + getFullName(field)
					+ " |  |  | fillcolor=\"cyan\" |\n";
		}
		return output;
	}

	/**
	 * Returns list of methods that are annotated as beans.
	 */
	private static ArrayList<JavaMethod> getConfigBeanMethods(
			JavaClass configClass) {
		ArrayList<JavaMethod> beanMethods = new ArrayList<JavaMethod>();
		for (JavaMethod method : configClass.getMethods()) {
			if (isBean(method)) {
				beanMethods.add(method);
			}
		}
		return beanMethods;
	}

	/**
	 * Make the first character of the method uppercase.
	 */
	private static String makeFirstLetterUppercase(String str) {
		return Character.toUpperCase(str.charAt(0)) + str.substring(1);
	}

	/**
	 * If object name is the same as class name it prints just object name else
	 * it creates correct name new line and class name.
	 */
	// TODO: instead getFullyQualifiedName() use getName and you do not have to
	// remove package
	private static String getFullObjectName(String name, String type) {
		type = type.substring(type.lastIndexOf(".") + 1);
		if (makeFirstLetterUppercase(name).equals(type)) {
			return name;
		}
		return name + " :\\l" + type;
	}

	private static String getFullName(JavaParameter parameter) {
		return getFullObjectName(parameter.getName(), parameter.getType()
				.getFullyQualifiedName());
	}

	private static String getFullName(JavaMethod method) {
		return getFullObjectName(method.getName(), method.getReturns()
				.getFullyQualifiedName());
	}

	private static String getFullName(JavaField field) {
		return getFullObjectName(field.getName(), field.getType()
				.getFullyQualifiedName());
	}

	private static String getAnnotationValue(JavaParameter parameter) {
		for (Annotation a : parameter.getAnnotations()) {
			if ("org.springframework.beans.factory.annotation.Value".equals(a
					.getType().getFullyQualifiedName())) {
				return a.getNamedParameter("value").toString();
			}
		}
		return null;
	}

	/**
	 * Escapes the name so that confluence can display it correctly.
	 */
	private static String escapeValueNameForConfluence(String string) {
		return string.replace("\"", "").replace("{", "\\{").replace("}", "\\}");
	}

	/**
	 * Returns proper color name depending on the type of object. Type of object
	 * is recognised by its last name.
	 */
	private static String getColorForObjectName(String name) {
		if (name.endsWith("Controller")) {
			return "green";
		} else if (name.endsWith("Service")) {
			return "red";
		}
		return "lightyellow";
	}

	/**
	 * Returns true if method is annotated as bean.
	 */
	private static boolean isBean(JavaMethod m) {
		for (Annotation a : m.getAnnotations()) {
			if ("org.springframework.context.annotation.Bean".equals(a
					.getType().getFullyQualifiedName())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if field is marked autowired which can mean it is a bean.
	 */
	private static boolean isAutowired(JavaField m) {
		for (Annotation a : m.getAnnotations()) {
			if ("org.springframework.beans.factory.annotation.Autowired"
					.equals(a.getType().getFullyQualifiedName())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if class is annotated as configuration.
	 */
	private static boolean isConfiguration(JavaClass c) {
		Annotation[] annotations = c.getAnnotations();
		for (Annotation annotation : annotations) {
			if ("org.springframework.context.annotation.Configuration"
					.equals(annotation.getType().getFullyQualifiedName())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns the list of all classes annotated as configuration.
	 */
	private static ArrayList<JavaClass> findConfigurationClasses(
			JavaDocBuilder builder) {
		ArrayList<JavaClass> configurationClasses = new ArrayList<JavaClass>();

		JavaClass[] classes = builder.getClasses();
		for (JavaClass c : classes) {
			if (isConfiguration(c)) {
				configurationClasses.add(c);
			}
		}
		return configurationClasses;
	}

}
